from random import shuffle

from hypothesis import given
from hypothesis import strategies as st

from plenum.simulation.helper import some_event, check_event_stream_invariants, SomeEvent, MAX_EVENTS_SIZE, some_events
from plenum.simulation.sim_event_stream import sim_events, RandomEventStream, sim_event_stream, ListEventStream, \
    CompositeEventStream


@st.composite
def some_sim_events(draw):
    min_size = draw(st.integers(min_value=0, max_value=10))
    max_size = draw(st.integers(min_value=min_size, max_value=10))
    min_interval = draw(st.integers(min_value=1, max_value=100))
    max_interval = draw(st.integers(min_value=min_interval, max_value=100))
    start_ts = draw(st.integers(min_value=-1000, max_value=1000))
    return min_size, max_size, min_interval, max_interval, start_ts, \
        draw(sim_events(some_event(), min_size=min_size, max_size=max_size,
                        min_interval=min_interval, max_interval=max_interval,
                        start_ts=start_ts))


@given(params_and_events=some_sim_events())
def test_sim_events(params_and_events):
    min_size, max_size, min_interval, max_interval, start_ts, events = params_and_events
    check_event_stream_invariants(events)

    assert min_size <= len(events) <= max_size

    # Random event stream should only contain expected events
    assert all(isinstance(ev.payload, SomeEvent) for ev in events)

    # All timestamps must be higher than start_ts
    assert all(ev.timestamp >= start_ts for ev in events)

    # Intervals between events should be in defined interval with some tolerance
    assert all(min_interval <= b.timestamp - a.timestamp <= max_interval
               for a, b in zip(events, events[1:]))


@st.composite
def random_event_stream(draw):
    min_interval = draw(st.integers(min_value=1, max_value=1000))
    max_interval = draw(st.integers(min_value=min_interval, max_value=1000))
    stream = RandomEventStream(draw, some_event(),
                               min_interval, max_interval)
    events = draw(sim_event_stream(stream, max_size=MAX_EVENTS_SIZE))
    return min_interval, max_interval, events


@given(params_and_events=random_event_stream())
def test_random_event_stream_properties(params_and_events):
    min_interval, max_interval, events = params_and_events
    check_event_stream_invariants(events)

    # Maximum allowed number of events should be generated
    assert len(events) == MAX_EVENTS_SIZE

    # Random event stream should only contain expected events
    assert all(isinstance(ev.payload, SomeEvent) for ev in events)

    # Intervals between events should be in defined interval with some tolerance
    assert all(0.999 * min_interval <= b.timestamp - a.timestamp <= 1.001 * max_interval
               for a, b in zip(events, events[1:]))


@st.composite
def list_event_stream(draw):
    stream = ListEventStream(draw(some_events(min_size=1)))

    # Get some timestamps stats
    events = stream.events
    min_ts = min(ev.timestamp for ev in events)
    max_ts = max(ev.timestamp for ev in events)
    duration = max_ts - min_ts

    # Add some random out of order events
    start_ts = min_ts - duration
    max_size = MAX_EVENTS_SIZE // 5
    max_interval = 3 * duration // max_size  # So that events are generated in range [min_ts-duration, max_ts+duration]
    more_events = draw(some_events(min_size=1, max_size=max_size,
                                   min_interval=max_interval // 5,
                                   max_interval=max_interval,
                                   start_ts=start_ts))
    shuffle(more_events)
    stream.extend(more_events)

    # Generate events
    input = stream.events
    events = draw(sim_event_stream(stream, max_size=MAX_EVENTS_SIZE))
    return input, events


@given(input_and_events=list_event_stream())
def test_list_event_stream_properties(input_and_events):
    input, events = input_and_events
    check_event_stream_invariants(events)

    # Events generated by event stream should be same as input truncated to maximum allowed number
    assert input[:MAX_EVENTS_SIZE] == events


@st.composite
def composite_two_normal_event_streams(draw):
    input_a = draw(some_events(max_size=MAX_EVENTS_SIZE // 2))
    input_b = draw(some_events(max_size=MAX_EVENTS_SIZE // 2))
    stream_a = ListEventStream(input_a)
    stream_b = ListEventStream(input_b)
    stream = CompositeEventStream(stream_a, stream_b)
    events = draw(sim_event_stream(stream, max_size=MAX_EVENTS_SIZE))
    return input_a, input_b, events


@given(inputs_and_events=composite_two_normal_event_streams())
def test_composite_two_normal_event_streams_properties(inputs_and_events):
    input_a, input_b, events = inputs_and_events
    check_event_stream_invariants(events)

    # Number of generated events should be same as number of input events
    assert len(input_a) + len(input_b) == len(events)

    # All input events should be present in generated events
    assert all(ev in events for ev in input_a)
    assert all(ev in events for ev in input_b)


@st.composite
def composite_normal_and_random_event_stream(draw):
    input = draw(some_events(max_size=MAX_EVENTS_SIZE))
    stream_a = RandomEventStream(draw, some_event())
    stream_b = ListEventStream(input)
    stream = CompositeEventStream(stream_a, stream_b)
    events = draw(sim_event_stream(stream, max_size=MAX_EVENTS_SIZE))
    return input, events


@given(input_and_events=composite_normal_and_random_event_stream())
def test_composite_normal_and_random_event_stream_properties(input_and_events):
    input, events = input_and_events
    check_event_stream_invariants(events)

    # Maximum allowed number of events should be generated
    assert len(events) == MAX_EVENTS_SIZE

    # Random event stream should only contain expected events
    assert all(isinstance(ev.payload, SomeEvent) for ev in events)

    # Assert all fixed events are added to output unless they are too late
    # assert all(ev in events for ev in input if ev.timestamp <= events[-1].timestamp)
